#include "SearchListView.h"

#include <QHBoxLayout>
#include <QLabel>
#include <QSplitter>
#include <QTimer>
#include <QVBoxLayout>

#include "FlickerThread.h"
#include "MethodInvoker.h"

SearchListView::SearchListView(QWidget* parent,
                               AbstractSearchList* abstractSearchList,
                               bool enableRegex, bool enableLock)
    : QWidget(parent), mAbstractSearchList(abstractSearchList) {
  setContextMenuPolicy(Qt::CustomContextMenu);

  // Create the main button/bar view with QSplitter
  //
  // |- Splitter --------------------------------------------|
  // | LISTVIEW                                              |
  // | SEARCH: | SEARCH BOX | LOCK CHECKBOX | REGEX CHECKBOX |
  // |-------------------------------------------------------|
  QSplitter* barSplitter = new QSplitter(Qt::Vertical);
  {
    // Create list layout (contains both ListViews)
    {
      // Initially hide the search list
      abstractSearchList->searchList()->hide();

      // Vertical layout
      QVBoxLayout* listLayout = new QVBoxLayout();
      listLayout->setContentsMargins(0, 0, 0, 0);
      listLayout->setSpacing(0);
      listLayout->addWidget(abstractSearchList->list());
      listLayout->addWidget(abstractSearchList->searchList());

      // Add list placeholder
      QWidget* listPlaceholder = new QWidget(this);
      listPlaceholder->setLayout(listLayout);

      barSplitter->addWidget(listPlaceholder);
    }

    // Filtering elements
    {
      // Input box
      mSearchBox = new QLineEdit();
      mSearchBox->setPlaceholderText(tr("Type here to filter results..."));

      // Regex parsing checkbox
      mRegexCheckbox = new QCheckBox(tr("Regex"));
      mRegexCheckbox->setTristate(true);

      // Lock checkbox
      mLockCheckbox = new QCheckBox(tr("Lock"));

      if (!enableRegex) mRegexCheckbox->hide();

      if (!enableLock) mLockCheckbox->hide();

      // Horizontal layout
      QHBoxLayout* horzLayout = new QHBoxLayout();
      horzLayout->setContentsMargins(4, 0, (enableRegex || enableLock) ? 0 : 4,
                                     0);
      horzLayout->setSpacing(2);
      horzLayout->addWidget(new QLabel(tr("Search: ")));
      horzLayout->addWidget(mSearchBox);
      horzLayout->addWidget(mLockCheckbox);
      horzLayout->addWidget(mRegexCheckbox);

      // Add searchbar placeholder
      QWidget* horzPlaceholder = new QWidget(this);
      horzPlaceholder->setLayout(horzLayout);

      barSplitter->addWidget(horzPlaceholder);
    }

    // Minimum size for the search box
    barSplitter->setStretchFactor(0, 1000);
    barSplitter->setStretchFactor(0, 1);

    // Disable main splitter
    for (int i = 0; i < barSplitter->count(); i++)
      barSplitter->handle(i)->setEnabled(false);
  }

  // Set the main layout which holds the splitter
  QVBoxLayout* mainLayout = new QVBoxLayout();
  mainLayout->setContentsMargins(0, 0, 0, 0);
  mainLayout->addWidget(barSplitter);
  setLayout(mainLayout);

  // Set global variables
  mCurList = abstractSearchList->list();
  mSearchStartCol = 0;

  // Install input event filter
  mSearchBox->installEventFilter(this);
  if (parent) mSearchBox->setWindowTitle(parent->metaObject()->className());

  // Setup search menu action
  mSearchAction = new QAction(DIcon("find.png"), tr("Search..."), this);
  connect(mSearchAction, SIGNAL(triggered()), this, SLOT(searchSlot()));

  // https://wiki.qt.io/Delay_action_to_wait_for_user_interaction
  mTypingTimer = new QTimer(this);
  mTypingTimer->setSingleShot(true);
  connect(mTypingTimer, SIGNAL(timeout()), this, SLOT(filterEntries()));

  // Slots
  connect(abstractSearchList->list(), SIGNAL(contextMenuSignal(QPoint)), this,
          SLOT(listContextMenu(QPoint)));
  connect(abstractSearchList->list(), SIGNAL(doubleClickedSignal()), this,
          SLOT(doubleClickedSlot()));
  connect(abstractSearchList->searchList(), SIGNAL(contextMenuSignal(QPoint)),
          this, SLOT(listContextMenu(QPoint)));
  connect(abstractSearchList->searchList(), SIGNAL(doubleClickedSignal()), this,
          SLOT(doubleClickedSlot()));
  connect(mSearchBox, SIGNAL(textEdited(QString)), this,
          SLOT(searchTextEdited(QString)));
  connect(mRegexCheckbox, SIGNAL(stateChanged(int)), this,
          SLOT(on_checkBoxRegex_stateChanged(int)));
  connect(mLockCheckbox, SIGNAL(toggled(bool)), mSearchBox,
          SLOT(setDisabled(bool)));

  // List input should always be forwarded to the filter edit
  abstractSearchList->searchList()->setFocusProxy(mSearchBox);
  abstractSearchList->list()->setFocusProxy(mSearchBox);
}

bool SearchListView::findTextInList(AbstractStdTable* list, QString text,
                                    int row, int startcol, bool startswith) {
  int count = list->getColumnCount();
  if (startcol + 1 > count) return false;
  if (startswith) {
    for (int i = startcol; i < count; i++)
      if (list->getCellContent(row, i).startsWith(text, Qt::CaseInsensitive))
        return true;
  } else {
    for (int i = startcol; i < count; i++) {
      auto state = mRegexCheckbox->checkState();
      if (state != Qt::Unchecked) {
        if (list->getCellContent(row, i).contains(QRegExp(
                text, state == Qt::PartiallyChecked ? Qt::CaseInsensitive
                                                    : Qt::CaseSensitive)))
          return true;
      } else {
        if (list->getCellContent(row, i).contains(text, Qt::CaseInsensitive))
          return true;
      }
    }
  }
  return false;
}

void SearchListView::filterEntries() {
  mAbstractSearchList->lock();

  // store the first selection value
  QString mLastFirstColValue;
  auto selList = mCurList->getSelection();
  if (!selList.empty() && mCurList->isValidIndex(selList[0], 0))
    mLastFirstColValue = mCurList->getCellContent(selList[0], 0);

  // get the correct previous list instance
  auto mPrevList = mAbstractSearchList->list()->isVisible()
                       ? mAbstractSearchList->list()
                       : mAbstractSearchList->searchList();

  if (mFilterText.length()) {
    MethodInvoker::invokeMethod([this]() {
      mAbstractSearchList->list()->hide();
      mAbstractSearchList->searchList()->show();
    });

    mCurList = mAbstractSearchList->searchList();

    // filter the list
    auto filterType = AbstractSearchList::FilterContainsTextCaseInsensitive;
    switch (mRegexCheckbox->checkState()) {
      case Qt::PartiallyChecked:
        filterType = AbstractSearchList::FilterRegexCaseInsensitive;
        break;
      case Qt::Checked:
        filterType = AbstractSearchList::FilterRegexCaseSensitive;
        break;
    }
    mAbstractSearchList->filter(mFilterText, filterType, mSearchStartCol);
  } else {
    MethodInvoker::invokeMethod([this]() {
      mAbstractSearchList->searchList()->hide();
      mAbstractSearchList->list()->show();
    });

    mCurList = mAbstractSearchList->list();
  }

  // attempt to restore previous selection
  bool hasSetSingleSelection = false;
  if (!mLastFirstColValue.isEmpty()) {
    int rows = mCurList->getRowCount();
    mCurList->setTableOffset(0);
    for (int i = 0; i < rows; i++) {
      if (mCurList->getCellContent(i, 0) == mLastFirstColValue) {
        if (rows > mCurList->getViewableRowsCount()) {
          int cur = i - mCurList->getViewableRowsCount() / 2;
          if (!mCurList->isValidIndex(cur, 0)) cur = i;
          mCurList->setTableOffset(cur);
        }
        mCurList->setSingleSelection(i);
        hasSetSingleSelection = true;
        break;
      }
    }
  }
  if (!hasSetSingleSelection) mCurList->setSingleSelection(0);

  if (!mCurList->getRowCount()) emit emptySearchResult();

  // Do not highlight with regex
  // TODO: fully respect highlighting mode
  if (mRegexCheckbox->checkState() == Qt::Unchecked)
    mAbstractSearchList->searchList()->setHighlightText(mFilterText,
                                                        mSearchStartCol);
  else
    mAbstractSearchList->searchList()->setHighlightText(QString());

  // Reload the search list data
  mAbstractSearchList->searchList()->reloadData();

  // setup the same layout of the previous list control
  if (mPrevList != mCurList) {
    int cols = mPrevList->getColumnCount();
    for (int i = 0; i < cols; i++) {
      mCurList->setColumnOrder(i, mPrevList->getColumnOrder(i));
      mCurList->setColumnHidden(i, mPrevList->getColumnHidden(i));
      mCurList->setColumnWidth(i, mPrevList->getColumnWidth(i));
    }
  }

  mAbstractSearchList->unlock();
}

void SearchListView::searchTextEdited(const QString& text) {
  mFilterText = text;
  mAbstractSearchList->lock();
  mTypingTimer->setInterval([](dsint rowCount) {
    // These numbers are kind of arbitrarily chosen, but seem to work
    if (rowCount <= 10000)
      return 0;
    else if (rowCount <= 600000)
      return 100;
    else
      return 350;
  }(mAbstractSearchList->list()->getRowCount()));
  mAbstractSearchList->unlock();
  mTypingTimer->start();  // This will fire filterEntries after interval ms.
  // If the user types something before it fires, the timer restarts counting
}

void SearchListView::refreshSearchList(const QString &filter) {
  mFilterText = filter;
  mSearchBox->setText(filter);
  filterEntries();
}

void SearchListView::clearFilter() {
  bool isFilterAlreadyEmpty = mFilterText.isEmpty();
  mFilterText.clear();

  if (!isFilterAlreadyEmpty) {
    MethodInvoker::invokeMethod([this]() { mSearchBox->clear(); });

    filterEntries();
  }
}

void SearchListView::listContextMenu(const QPoint& pos) {
  QMenu wMenu(this);
  emit listContextMenuSignal(&wMenu);
  wMenu.addSeparator();
  wMenu.addAction(mSearchAction);
  QMenu wCopyMenu(tr("&Copy"), this);
  wCopyMenu.setIcon(DIcon("copy.png"));
  mCurList->setupCopyMenu(&wCopyMenu);
  if (wCopyMenu.actions().length()) wMenu.addMenu(&wCopyMenu);
  wMenu.exec(mCurList->mapToGlobal(pos));
}

void SearchListView::doubleClickedSlot() { emit enterPressedSignal(); }

void SearchListView::on_checkBoxRegex_stateChanged(int state) {
  QString tooltip;
  switch (state) {
    default:
    case Qt::Unchecked:
      // No tooltip
      break;
    case Qt::Checked:
      tooltip = tr("Use case sensitive regular expression");
      break;
    case Qt::PartiallyChecked:
      tooltip = tr("Use case insensitive regular expression");
      break;
  }
  mRegexCheckbox->setToolTip(tooltip);

  refreshSearchList();
}

bool SearchListView::isSearchBoxLocked() { return mLockCheckbox->isChecked(); }

bool SearchListView::eventFilter(QObject* obj, QEvent* event) {
  // Keyboard button press being sent to the QLineEdit
  if (obj == mSearchBox && event->type() == QEvent::KeyPress) {
    QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);

    switch (keyEvent->key()) {
      // The user pressed enter/return
      case Qt::Key_Return:
      case Qt::Key_Enter:
        if (mCurList->getCellContent(mCurList->getInitialSelection(), 0)
                .length())
          emit enterPressedSignal();
        return true;

      // Search box misc controls
      case Qt::Key_Escape:
        mSearchBox->clear();
      case Qt::Key_Left:
      case Qt::Key_Right:
      case Qt::Key_Backspace:
      case Qt::Key_Delete:
      case Qt::Key_Home:
      case Qt::Key_End:
      case Qt::Key_Insert:
        return QWidget::eventFilter(obj, event);

      // Search box shortcuts
      case Qt::Key_V:  // Ctrl+V
      case Qt::Key_X:  // Ctrl+X
      case Qt::Key_Z:  // Ctrl+Z
      case Qt::Key_A:  // Ctrl+A
      case Qt::Key_Y:  // Ctrl+Y
        if (keyEvent->modifiers() == Qt::CTRL)
          return QWidget::eventFilter(obj, event);
    }

    // Printable characters go to the search box
    char key = keyEvent->text().toUtf8().constData()[0];

    if (isprint(key)) return QWidget::eventFilter(obj, event);

    // By default, all other keys are forwarded to the search view
    return QApplication::sendEvent(mCurList, event);
  }

  return QWidget::eventFilter(obj, event);
}

void SearchListView::searchSlot() {
  FlickerThread* thread = new FlickerThread(mSearchBox, this);
  connect(thread, SIGNAL(setStyleSheet(QString)), mSearchBox,
          SLOT(setStyleSheet(QString)));
  connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
  thread->start();
}
